package lab.poc.erl.otp;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import lab.poc.erl.Actor;
import lab.poc.erl.Exit;
import lab.poc.erl.Match;
import lab.poc.erl.Node;
import lab.poc.erl.Pattern;
import lab.poc.erl.Patterns;
import lab.poc.erl.Pid;
import lab.poc.erl.TrapExitMessage;

// what a mess - this needs to be redone from scratch.
@Deprecated
public abstract class Supervisor extends Actor {

	private static enum MessageKind {
		TRAP_EXIT,
		SHUTDOWN,
		UNKNOWN
	}

	private static final class TrapExitNotification implements Pattern {
		static final TrapExitNotification P = new TrapExitNotification();

		@Override
		public boolean approve(final Object msg) {
			if (msg instanceof TrapExitMessage) {
				return true;
			}
			return false;
		}
		@Override
		@SuppressWarnings("unchecked")
		public MessageKind type() {
			return MessageKind.TRAP_EXIT;
		}
	}

	private static final class ShutdownCommand implements Pattern {
		static final ShutdownCommand P = new ShutdownCommand();

		@Override
		public boolean approve(final Object msg) {
			if (msg instanceof ShutdownMessage) {
				return true;
			}
			return false;
		}
		@Override
		@SuppressWarnings("unchecked")
		public MessageKind type() {
			return MessageKind.SHUTDOWN;
		}
	}

	private final LinkedList<PidWithChildSpec> childrenPids = new LinkedList<PidWithChildSpec>();
	private final HashMap<Pid, ChildSpec> pidLookup = new HashMap<Pid, ChildSpec>();
	private SupervisorSpec supervisorSpec;
	private int restartsWithinTimeWindow = 0;
	private long restartIntervalBegin = 0L;

	protected abstract SupervisorSpec getSupervisorSpec();

	protected abstract List<ChildSpec> getChildrenSpec();

	private boolean isProcessRestartable() {
		if (restartIntervalBegin == 0L) {
			restartIntervalBegin = System.currentTimeMillis();
		}
		++restartsWithinTimeWindow;
		if (restartsWithinTimeWindow >= supervisorSpec.maxAllowedRestarts) {
			restartsWithinTimeWindow = 0;
			if (System.currentTimeMillis() - restartIntervalBegin <= supervisorSpec.withinSeconds * 1000L) {
				restartIntervalBegin = System.currentTimeMillis();
				return false;
			}
			restartIntervalBegin = System.currentTimeMillis();
		}
		return true;
	}

	@Override
	protected final void onSpawn(final List<Object> args) {
		trapExit(true);

		supervisorSpec = getSupervisorSpec();
		if (supervisorSpec == null) {
			throw new IllegalArgumentException("SupervisorSpec must not be null");
		}

		final List<ChildSpec> childrenSpecs = getChildrenSpec();
		if (childrenSpecs == null || childrenSpecs.isEmpty()) {
			throw new IllegalArgumentException("ChildrenSpec list must not be null or empty");
		}
		startChildren(childrenSpecs);

		loop();
	}

	private void loop() {
		boolean listen = true;

		while (listen) {
			Match m = receive(TrapExitNotification.P, ShutdownCommand.P, Patterns.AnyMsg);
			
			switch (m.pat.<MessageKind>type()) {
			case TRAP_EXIT: {
				TrapExitMessage msg = (TrapExitMessage) m.msg;
				Pid pid = msg.exitingPid;
				if (parent().equals(pid)) {
					// parent supervisor has died, kill all children and exit
					listen = false;
					abort();
					break;
				}
				if (!pidLookup.containsKey(pid)) {
					// ignore any Pids we don't care about
					continue;
				}

				ChildSpec childSpec = pidLookup.get(pid);
				Exit exitType = msg.exitType;
				if (childSpec.restart == ChildSpec.RestartType.TRANSIENT
						|| (childSpec.restart == ChildSpec.RestartType.TEMPORARY && exitType == Exit.NORMAL)) {
					// no restart in this case, simply remove the actors and forget them
					getNode().unregister(childSpec.id);
					pidLookup.remove(pid);
					childrenPids.remove(pid);
					continue;
				}

				getNode().unregister(childSpec.id);
				// check whether max allowed number of restarts within
				// given period as per supervisor spec has been reached
				// and abort anything if this is true.
				if (!isProcessRestartable()) {
					listen = false;
					abort();
					break;
				}

				switch (supervisorSpec.restartStrategy) {
				case ONE_FOR_ONE: {
					pidLookup.remove(pid);
					int startPos = childrenPids.indexOf(pid);
					if (startPos == -1) {
						throw new IllegalStateException();
					}
					childrenPids.set(startPos, null);
					try {
						Pid newChildPid = getNode().spawnLink(
								childSpec.startSpec.actorClass,
								childSpec.startSpec.arguments);
						getNode().register(childSpec.id, newChildPid);
						childrenPids.set(startPos, new PidWithChildSpec(
								newChildPid, childSpec));
						pidLookup.put(newChildPid, childSpec);
					} catch (Throwable t) {
						abort();
						if (t instanceof RuntimeException) {
							throw (RuntimeException) t;
						}
						if (t instanceof Error) {
							throw (Error) t;
						}
					}
				}
					break;
				case ONE_FOR_ALL: {
					ArrayList<ChildSpec> newChildrenSpecs = new ArrayList<ChildSpec>();
					for (PidWithChildSpec pidSpec : childrenPids) {
						if (pidSpec == null) {
							continue;
						}
						if (pidSpec.spec.restart != ChildSpec.RestartType.TRANSIENT) {
							// transient processes won't get restarted
							newChildrenSpecs.add(pidSpec.spec);
						}
					}
					killAllChildren();
					childrenPids.clear();
					pidLookup.clear();
					if (!newChildrenSpecs.isEmpty()) {
						startChildren(newChildrenSpecs);
					}
				}
					break;
				case REST_FOR_ONE: {
					pidLookup.remove(pid);
					int startPos = childrenPids.indexOf(pid);
					if (startPos == -1) {
						throw new IllegalStateException();
					}
					childrenPids.set(startPos, null);
					ArrayList<Pid> childrenToKill = new ArrayList<Pid>();
					for (int i = childrenPids.size() - 1; i > startPos; i--) {
						PidWithChildSpec pidSpec = childrenPids.get(i);
						if (pidSpec != null) {
							getNode().unregister(pidSpec.spec.id);
							childrenToKill.add(pidSpec.pid);
							pidLookup.remove(pidSpec.pid);
						}
					}
					ArrayList<ChildSpec> newChildrenSpecs = new ArrayList<ChildSpec>();
					newChildrenSpecs.add(childSpec);
					for (int i = startPos + 1; i < childrenPids.size(); ++i) {
						PidWithChildSpec pidSpec = childrenPids.get(i);
						if (pidSpec == null) {
							continue;
						}
						if (pidSpec.spec.restart != ChildSpec.RestartType.TRANSIENT) {
							// transient processes won't get restarted
							newChildrenSpecs.add(pidSpec.spec);
						}
						childrenPids.set(i, null);
					}
					killSpecifiedChildren(childrenToKill);
					removeNullsFromChildrenList();
					if (!newChildrenSpecs.isEmpty()) {
						startChildren(newChildrenSpecs);
					}
				}
					break;
				default: {
					throw new IllegalStateException();
				}
				}
			}
			break;
			case SHUTDOWN: {
				// shutdown requested, kill all children and exit
				listen = false;
				abort();
			}
			break;
			default: {
				// ignore and throw away
			}
			break;
			}
		}
	}

	private LinkedList<PidWithChildSpec> startChildren(final List<ChildSpec> childrenSpecs) {
		Node localNode = getNode();
		for (ChildSpec spec : childrenSpecs) {
			StartSpec startSpec = spec.startSpec;
			try {
				Pid childPid = localNode.spawnLink(startSpec.actorClass,
						startSpec.arguments);
				localNode.register(spec.id, childPid);
				childrenPids.add(new PidWithChildSpec(childPid, spec));
				pidLookup.put(childPid, spec);
			} catch (Throwable t) {
				abort();
				if (t instanceof RuntimeException) {
					throw (RuntimeException) t;
				}
				if (t instanceof Error) {
					throw (Error) t;
				}
			}
		}
		return childrenPids;
	}

	private void killAllChildren() {
		try {
			Collections.reverse(childrenPids);
			for (Iterator<PidWithChildSpec> it = childrenPids.iterator(); it.hasNext(); ) {
				try {
					PidWithChildSpec pidSpec = it.next();
					Pid childPid = null;
					if (pidSpec != null) {
						getNode().unregister(pidSpec.spec.id);
						childPid = pidSpec.pid;
						childPid.exit();
					}
					it.remove();
					pidLookup.remove(childPid);
				} catch (Throwable ignore) {
				}
			}
		} catch (Throwable ignore) {
		}
	}

	private void killSpecifiedChildren(List<Pid> childrenPidList) {
		for (Pid childPid : childrenPidList) {
			if (childPid != null) {
				try {
					childPid.exit();
				} catch (Throwable ignore) {
				}
			}
		}
	}

	private void abort() {
		killAllChildren();
		exit();
	}

	private void removeNullsFromChildrenList() {
		for (Iterator<PidWithChildSpec> it = childrenPids.iterator(); it.hasNext(); ) {
			PidWithChildSpec pidSpec = it.next();
			if (pidSpec == null) {
				it.remove();
			}
		}
	}

	private static final class PidWithChildSpec {
		final Pid pid;
		final ChildSpec spec;

		PidWithChildSpec(final Pid pid, final ChildSpec spec) {
			this.pid = pid;
			this.spec = spec;
		}

		@Override
		public boolean equals(Object obj) {
			return pid.equals(obj);
		}

		@Override
		public int hashCode() {
			return pid.hashCode();
		}
	}
}
